/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mariotaku.gallery3d.ui;

import org.mariotaku.twidere.R;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;

// This is copied from android.widget.EdgeEffect with some small modifications:
// (1) Copy the images (overscroll_{edge|glow}.png) to local resources.
// (2) Use "GLCanvas" instead of "Canvas" for draw()'s parameter.
// (3) Use a private Drawable class (which inherits from ResourceTexture)
//     instead of android.graphics.drawable.Drawable to hold the images.
//     The private Drawable class is used to translate original Canvas calls to
//     corresponding GLCanvas calls.

/**
 * This class performs the graphical effect used at the edges of scrollable
 * widgets when the user scrolls beyond the content bounds in 2D space.
 * 
 * <p>
 * EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
 * instance for each edge that should show the effect, feed it input data using
 * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and
 * {@link #onRelease()}, and draw the effect using {@link #draw(Canvas)} in the
 * widget's overridden {@link android.view.View#draw(Canvas)} method. If
 * {@link #isFinished()} returns false after drawing, the edge effect's
 * animation is not yet complete and the widget should schedule another drawing
 * pass to continue the animation.
 * </p>
 * 
 * <p>
 * When drawing, widgets should draw their main content and child views first,
 * usually by invoking <code>super.draw(canvas)</code> from an overridden
 * <code>draw</code> method. (This will invoke onDraw and dispatch drawing to
 * child views as needed.) The edge effect may then be drawn on top of the
 * view's content using the {@link #draw(Canvas)} method.
 * </p>
 */
public class EdgeEffect {
	@SuppressWarnings("unused")
	private static final String TAG = "EdgeEffect";

	// Time it will take the effect to fully recede in ms
	private static final int RECEDE_TIME = 1000;

	// Time it will take before a pulled glow begins receding in ms
	private static final int PULL_TIME = 167;

	// Time it will take in ms for a pulled glow to decay to partial strength
	// before release
	private static final int PULL_DECAY_TIME = 1000;

	private static final float MAX_ALPHA = 0.8f;
	private static final float HELD_EDGE_SCALE_Y = 0.5f;
	private static final float MAX_GLOW_HEIGHT = 4.f;

	private static final float PULL_GLOW_BEGIN = 1.f;
	private static final float PULL_EDGE_BEGIN = 0.6f;

	// Minimum velocity that will be absorbed
	private static final int MIN_VELOCITY = 100;

	private static final float EPSILON = 0.001f;

	private final Drawable mEdge;
	private final Drawable mGlow;
	private int mWidth;
	private final int MIN_WIDTH = 300;
	private final int mMinWidth;

	private float mEdgeAlpha;
	private float mEdgeScaleY;
	private float mGlowAlpha;
	private float mGlowScaleY;

	private float mEdgeAlphaStart;
	private float mEdgeAlphaFinish;
	private float mEdgeScaleYStart;
	private float mEdgeScaleYFinish;
	private float mGlowAlphaStart;
	private float mGlowAlphaFinish;
	private float mGlowScaleYStart;
	private float mGlowScaleYFinish;

	private long mStartTime;
	private float mDuration;

	private final Interpolator mInterpolator;

	private static final int STATE_IDLE = 0;
	private static final int STATE_PULL = 1;
	private static final int STATE_ABSORB = 2;
	private static final int STATE_RECEDE = 3;
	private static final int STATE_PULL_DECAY = 4;

	// How much dragging should effect the height of the edge image.
	// Number determined by user testing.
	private static final int PULL_DISTANCE_EDGE_FACTOR = 7;

	// How much dragging should effect the height of the glow image.
	// Number determined by user testing.
	private static final int PULL_DISTANCE_GLOW_FACTOR = 7;
	private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f;

	private static final int VELOCITY_EDGE_FACTOR = 8;
	private static final int VELOCITY_GLOW_FACTOR = 16;

	private int mState = STATE_IDLE;

	private float mPullDistance;

	/**
	 * Construct a new EdgeEffect with a theme appropriate for the provided
	 * context.
	 * 
	 * @param context Context used to provide theming and resource information
	 *            for the EdgeEffect
	 */
	public EdgeEffect(final Context context) {
		mEdge = new Drawable(context, R.drawable.overscroll_edge);
		mGlow = new Drawable(context, R.drawable.overscroll_glow);

		mMinWidth = (int) (context.getResources().getDisplayMetrics().density * MIN_WIDTH + 0.5f);
		mInterpolator = new DecelerateInterpolator();
	}

	/**
	 * Draw into the provided canvas. Assumes that the canvas has been rotated
	 * accordingly and the size has been set. The effect will be drawn the full
	 * width of X=0 to X=width, beginning from Y=0 and extending to some factor
	 * < 1.f of height.
	 * 
	 * @param canvas Canvas to draw into
	 * @return true if drawing should continue beyond this frame to continue the
	 *         animation
	 */
	public boolean draw(final GLCanvas canvas) {
		update();

		final int edgeHeight = mEdge.getIntrinsicHeight();
		mEdge.getIntrinsicWidth();
		final int glowHeight = mGlow.getIntrinsicHeight();
		final int glowWidth = mGlow.getIntrinsicWidth();

		mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));

		final int glowBottom = (int) Math.min(glowHeight * mGlowScaleY * glowHeight / glowWidth * 0.6f, glowHeight
				* MAX_GLOW_HEIGHT);
		if (mWidth < mMinWidth) {
			// Center the glow and clip it.
			final int glowLeft = (mWidth - mMinWidth) / 2;
			mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom);
		} else {
			// Stretch the glow to fit.
			mGlow.setBounds(0, 0, mWidth, glowBottom);
		}

		mGlow.draw(canvas);

		mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));

		final int edgeBottom = (int) (edgeHeight * mEdgeScaleY);
		if (mWidth < mMinWidth) {
			// Center the edge and clip it.
			final int edgeLeft = (mWidth - mMinWidth) / 2;
			mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom);
		} else {
			// Stretch the edge to fit.
			mEdge.setBounds(0, 0, mWidth, edgeBottom);
		}
		mEdge.draw(canvas);

		return mState != STATE_IDLE;
	}

	/**
	 * Reports if this EdgeEffect's animation is finished. If this method
	 * returns false after a call to {@link #draw(Canvas)} the host widget
	 * should schedule another drawing pass to continue the animation.
	 * 
	 * @return true if animation is finished, false if drawing should continue
	 *         on the next frame.
	 */
	public boolean isFinished() {
		return mState == STATE_IDLE;
	}

	/**
	 * Call when the effect absorbs an impact at the given velocity. Used when a
	 * fling reaches the scroll boundary.
	 * 
	 * <p>
	 * When using a {@link android.widget.Scroller} or
	 * {@link android.widget.OverScroller}, the method
	 * <code>getCurrVelocity</code> will provide a reasonable approximation to
	 * use here.
	 * </p>
	 * 
	 * @param velocity Velocity at impact in pixels per second.
	 */
	public void onAbsorb(int velocity) {
		mState = STATE_ABSORB;
		velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));

		mStartTime = AnimationTime.get();
		mDuration = 0.1f + velocity * 0.03f;

		// The edge should always be at least partially visible, regardless
		// of velocity.
		mEdgeAlphaStart = 0.f;
		mEdgeScaleY = mEdgeScaleYStart = 0.f;
		// The glow depends more on the velocity, and therefore starts out
		// nearly invisible.
		mGlowAlphaStart = 0.5f;
		mGlowScaleYStart = 0.f;

		// Factor the velocity by 8. Testing on device shows this works best to
		// reflect the strength of the user's scrolling.
		mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
		// Edge should never get larger than the size of its asset.
		mEdgeScaleYFinish = Math.max(HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));

		// Growth for the size of the glow should be quadratic to properly
		// respond
		// to a user's scrolling speed. The faster the scrolling speed, the more
		// intense the effect should be for both the size and the saturation.
		mGlowScaleYFinish = Math.min(0.025f + velocity * (velocity / 100) * 0.00015f, 1.75f);
		// Alpha should change for the glow as well as size.
		mGlowAlphaFinish = Math.max(mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
	}

	/**
	 * A view should call this when content is pulled away from an edge by the
	 * user. This will update the state of the current visual effect and its
	 * associated animation. The host view should always
	 * {@link android.view.View#invalidate()} after this and draw the results
	 * accordingly.
	 * 
	 * @param deltaDistance Change in distance since the last call. Values may
	 *            be 0 (no change) to 1.f (full length of the view) or negative
	 *            values to express change back toward the edge reached to
	 *            initiate the effect.
	 */
	public void onPull(final float deltaDistance) {
		final long now = AnimationTime.get();
		if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) return;
		if (mState != STATE_PULL) {
			mGlowScaleY = PULL_GLOW_BEGIN;
		}
		mState = STATE_PULL;

		mStartTime = now;
		mDuration = PULL_TIME;

		mPullDistance += deltaDistance;
		final float distance = Math.abs(mPullDistance);

		mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
		mEdgeScaleY = mEdgeScaleYStart = Math.max(HELD_EDGE_SCALE_Y,
				Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));

		mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, mGlowAlpha + Math.abs(deltaDistance)
				* PULL_DISTANCE_ALPHA_GLOW_FACTOR);

		float glowChange = Math.abs(deltaDistance);
		if (deltaDistance > 0 && mPullDistance < 0) {
			glowChange = -glowChange;
		}
		if (mPullDistance == 0) {
			mGlowScaleY = 0;
		}

		// Do not allow glow to get larger than MAX_GLOW_HEIGHT.
		mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT,
				Math.max(0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));

		mEdgeAlphaFinish = mEdgeAlpha;
		mEdgeScaleYFinish = mEdgeScaleY;
		mGlowAlphaFinish = mGlowAlpha;
		mGlowScaleYFinish = mGlowScaleY;
	}

	/**
	 * Call when the object is released after being pulled. This will begin the
	 * "decay" phase of the effect. After calling this method the host view
	 * should {@link android.view.View#invalidate()} and thereby draw the
	 * results accordingly.
	 */
	public void onRelease() {
		mPullDistance = 0;

		if (mState != STATE_PULL && mState != STATE_PULL_DECAY) return;

		mState = STATE_RECEDE;
		mEdgeAlphaStart = mEdgeAlpha;
		mEdgeScaleYStart = mEdgeScaleY;
		mGlowAlphaStart = mGlowAlpha;
		mGlowScaleYStart = mGlowScaleY;

		mEdgeAlphaFinish = 0.f;
		mEdgeScaleYFinish = 0.f;
		mGlowAlphaFinish = 0.f;
		mGlowScaleYFinish = 0.f;

		mStartTime = AnimationTime.get();
		mDuration = RECEDE_TIME;
	}

	/**
	 * Set the size of this edge effect in pixels.
	 * 
	 * @param width Effect width in pixels
	 * @param height Effect height in pixels
	 */
	public void setSize(final int width, final int height) {
		mWidth = width;
	}

	private void update() {
		final long time = AnimationTime.get();
		final float t = Math.min((time - mStartTime) / mDuration, 1.f);

		final float interp = mInterpolator.getInterpolation(t);

		mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
		mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
		mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
		mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;

		if (t >= 1.f - EPSILON) {
			switch (mState) {
				case STATE_ABSORB:
					mState = STATE_RECEDE;
					mStartTime = AnimationTime.get();
					mDuration = RECEDE_TIME;

					mEdgeAlphaStart = mEdgeAlpha;
					mEdgeScaleYStart = mEdgeScaleY;
					mGlowAlphaStart = mGlowAlpha;
					mGlowScaleYStart = mGlowScaleY;

					// After absorb, the glow and edge should fade to nothing.
					mEdgeAlphaFinish = 0.f;
					mEdgeScaleYFinish = 0.f;
					mGlowAlphaFinish = 0.f;
					mGlowScaleYFinish = 0.f;
					break;
				case STATE_PULL:
					mState = STATE_PULL_DECAY;
					mStartTime = AnimationTime.get();
					mDuration = PULL_DECAY_TIME;

					mEdgeAlphaStart = mEdgeAlpha;
					mEdgeScaleYStart = mEdgeScaleY;
					mGlowAlphaStart = mGlowAlpha;
					mGlowScaleYStart = mGlowScaleY;

					// After pull, the glow and edge should fade to nothing.
					mEdgeAlphaFinish = 0.f;
					mEdgeScaleYFinish = 0.f;
					mGlowAlphaFinish = 0.f;
					mGlowScaleYFinish = 0.f;
					break;
				case STATE_PULL_DECAY:
					// When receding, we want edge to decrease more slowly
					// than the glow.
					final float factor = mGlowScaleYFinish != 0 ? 1 / (mGlowScaleYFinish * mGlowScaleYFinish)
							: Float.MAX_VALUE;
					mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp * factor;
					mState = STATE_RECEDE;
					break;
				case STATE_RECEDE:
					mState = STATE_IDLE;
					break;
			}
		}
	}

	private static class Drawable extends ResourceTexture {
		private final Rect mBounds = new Rect();
		private int mAlpha = 255;

		public Drawable(final Context context, final int resId) {
			super(context, resId);
		}

		public void draw(final GLCanvas canvas) {
			canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
			canvas.multiplyAlpha(mAlpha / 255.0f);
			final Rect b = mBounds;
			draw(canvas, b.left, b.top, b.width(), b.height());
			canvas.restore();
		}

		public int getIntrinsicHeight() {
			return getHeight();
		}

		public int getIntrinsicWidth() {
			return getWidth();
		}

		public void setAlpha(final int alpha) {
			mAlpha = alpha;
		}

		public void setBounds(final int left, final int top, final int right, final int bottom) {
			mBounds.set(left, top, right, bottom);
		}
	}
}
